#!/usr/bin/env python
#
# Copyright (C) Citrix Inc
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# Import appliances generated by boxgrinder into XenServer/XCP

from __future__ import print_function
import os, sys, time, socket, traceback, syslog

log_f = os.fdopen(os.dup(sys.stdout.fileno()), "aw")
pid = None
use_syslog = False

def reopenlog(log_file):
    global log_f
    if log_f:
        log_f.close()
    if log_file and log_file != "stdout:":
        log_f = open(log_file, "aw")
    elif log_file and log_file == "stdout:":
        log_f = os.fdopen(os.dup(sys.stdout.fileno()), "aw")

def log(txt):
    global log_f, pid, use_syslog
    if use_syslog:
        syslog.syslog(txt)
        return
    if not pid:
        pid = os.getpid()
    t = time.strftime("%Y%m%dT%H:%M:%SZ", time.gmtime())
    print("%s [%d] %s" % (t, pid, txt), file=log_f)
    log_f.flush()

# For reference, here's what the boxgrinder default output XML looks like
# Is there a definition somewhere?
example = """
<image>
  <name>centos-base</name>
  <domain>
    <boot type='hvm'>
      <guest>
        <arch>x86_64</arch>
      </guest>
      <os>
        <loader dev='hd'/>
      </os>
      <drive disk='centos-base-sda.raw' target='hda'/>
    </boot>
    <devices>
      <vcpu>1</vcpu>
      <memory>262144</memory>
      <interface/>
      <graphics/>
    </devices>
  </domain>
  <storage>
    <disk file='centos-base-sda.raw' use='system' format='raw'/>
  </storage>
</image>
"""

import xmlrpclib
class XCPError(Exception):
    def __init__(self, result):
        self.result = result
    def __str__(self):
        # {'Status': 'Failure', 'ErrorDescription': ['SESSION_AUTHENTICATION_FAILED', 'a', 'Authentication failure']}
        return " ".join(self.result["ErrorDescription"])

class Failure(Exception):
    def __init__(self, reason):
        self.reason = reason
    def __str__(self):
        return self.reason

def value(x):
    if "Value" in x:
        return x["Value"]
    else:
        raise XCPError(x)

# We base our VMs off this generic HVM template
base_template = "Other install media"

import xml.dom.minidom
import sys

# Creates the VM, VBDs and VDIs
def import_metadata(server, session, filename):
    doc = xml.dom.minidom.parse(filename)
    def getSingleElement(doc, name):
        elements = doc.getElementsByTagName(name)
        if len(elements) != 1:
            raise Failure("Expecting exactly one <%s> element" % name)
        return elements[0]
    image = getSingleElement(doc, "image")
    domain = getSingleElement(image, "domain")
    boot = getSingleElement(domain, "boot")
    devices = getSingleElement(domain, "devices")
    storage = getSingleElement(image, "storage")
    def getText(doc, name):
        nodes = doc.getElementsByTagName(name)
        if len(nodes) != 1:
            print("Expecting exactly one %s tag" % name, file=sys.stderr)
            sys.exit(1)
        result = ""
        for child in nodes[0].childNodes:
            if child.nodeType == child.TEXT_NODE:
                result = result + child.data
        return result
    def getAttr(doc, name):
        for (n, value) in doc.attributes.items():
            if name == n:
                return value
        return ""
    # Clone the "Other install media" template and inherit basic
    # properties from it.
    templates = value(server.VM.get_by_name_label(session, base_template))
    if len(templates) != 1:
        raise Failure("Expecting exactly one \"%s\" template" % base_template)
    template = templates[0]
    name = getText(image, "name")
    log("Cloning template %s into %s" % (base_template, name))
    vm = value(server.VM.clone(session, template, name))
    value(server.VM.set_is_a_template(session, vm, False))
    vcpu = getText(devices, "vcpu")
    if vcpu != "":
        log("Setting number of vCPUs to: %s" % vcpu)
        value(server.VM.set_VCPUs_max(session, vm, vcpu))
        value(server.VM.set_VCPUs_at_startup(session, vm, vcpu))
    memory = getText(devices, "memory") # KiB
    if memory != "":
        log("Setting memory to %s KiB" % memory)
        bytes = str(long(memory) * 1024)
        value(server.VM.set_memory_limits(session, vm, "0", bytes, bytes, bytes))
    boot_type = getAttr(boot, "type")
    if boot_type == "hvm":
        log("VM is set to HVM boot by default")
    else:
        log("Ignoring unknown boot type: %s" % boot_type)
    # Disks
    disks = storage.getElementsByTagName("disk")
    drives = boot.getElementsByTagName("drive")
    pool = value(server.pool.get_all(session))[0]
    sr = value(server.pool.get_default_SR(session, pool))
    try:
        log("Will create disks in the default SR: %s" % (value(server.SR.get_name_label(session, sr))))
    except Exception as e:
        log("Caught %s" % str(e))
        raise Failure("Default SR is not set on the pool (%s)" % sr)
    vdis = {}
    for disk in disks:
        ty = getAttr(disk, "format")
        if ty != "raw":
            raise Failure("Expected all disks to have format = raw")
        filename = getAttr(disk, "file")
        size = os.path.getsize(filename)
        _type = "user"
        if getAttr(disk, "use") == "system":
            _type = "system"
        vdi_info = {
            "name_label": filename,
            "name_description": "",
            "SR": sr,
            "virtual_size": str(size),
            "type": _type,
            "sharable": False,
            "read_only": False,
            "other_config": {},
            }
        vdi = value(server.VDI.create(session, vdi_info))
        log("Created VDI %s for %s" % (vdi, filename))
        vdis[filename] = vdi
    for drive in drives:
        disk = getAttr(drive, "disk")
        target = getAttr(drive, "target")
        vdi = vdis[disk]
        bootable = drive == drives[0]
        vbd_info = {
            "VM": vm,
            "VDI": vdi,
            "userdevice": target,
            "bootable": bootable,
            "mode": "RW",
            "type": "Disk",
            "empty": False,
            "other_config": { "owner": "true" },
            "qos_algorithm_type": "",
            "qos_algorithm_params": {},
            }
        vbd = value(server.VBD.create(session, vbd_info))
        log("Created VBD %s for %s" % (vbd, disk))
    return (vm, vdis)

CURL = "/usr/bin/curl"
if not(os.path.exists(CURL)):
    raise Failure("%s doesn't exist" % CURL)

import commands
def import_vdi(url, session, vdi, filename):
    cmd = "%s -T%s %s/import_raw_vdi?session_id=%s\&vdi=%s" % (CURL, filename, url, session, vdi)
    log("%s" % cmd)
    (code, output) = commands.getstatusoutput(cmd)
    if code != 0:
        log("Disk upload failed: %s" % output)
        raise Failure("disk upload failed")

if __name__ == "__main__":
    from optparse import OptionParser

    settings = {
        "log": "stdout:",
        "server": "http://127.0.0.1",
        "username": "root",
        "password": "",
        }

    log("settings = %s" % repr(settings))
    
    parser = OptionParser(usage="usage: %prog [options] filename.xml")
    parser.add_option("-l", "--log", dest="logfile", help="log to LOG", metavar="LOG")
    parser.add_option("-s", "--server", dest="server", help="connect to SERVER", metavar="SERVER")
    parser.add_option("-u", "--username", dest="username", help="login as USERNAME", metavar="USERNAME")
    parser.add_option("-p", "--password", dest="password", help="use password PASSWORD", metavar="PASSWORD")
    (options, args) = parser.parse_args()
    if len(args) != 1:
        parser.error("wrong number of arguments")

    options = options.__dict__

    for setting in settings:
        if setting in options and options[setting]:
            settings[setting] = options[setting]
            s = repr(settings[setting])
            if setting == "password":
                s = "*REDACTED*"
            log("option settings[%s] <- %s" % (setting, s))

    if settings["log"] == "syslog:":
        use_syslog = True
        reopenlog(None)
    elif settings["log"] == "stdout:":
        use_syslog = False
        reopenlog("stdout:")
    else:
        use_syslog = False
        reopenlog(settings["log"])

    server = xmlrpclib.Server(settings["server"])
    session = value(server.session.login_with_password(settings["username"], settings["password"], "1.0", "xen-api-scripts-import-boxgrinder"))
    try:
        (vm, vdis) = import_metadata(server, session, args[0])
        for filename in vdis.keys():
            import_vdi(settings["server"], session, vdis[filename], filename)
        log("VM import complete")
        log("%s" % vm)
    finally:
        value(server.session.logout(session))
